package dev.mrchen.flows.core;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;

public class ExecuteEngine {

    private final FlowInfo preFlow;

    private final FlowInfo mainFlow;

    private final FlowInfo exceptionFlow;

    public ExecuteEngine(
            Iterable<Executor> preExecutors ,
            Iterable<Executor> mainExecutors ,
            Iterable<Executor> exceptionExecutors
    ) {
        this.preFlow = new FlowChain(preExecutors.iterator());
        this.mainFlow = new FlowChain(mainExecutors.iterator());
        this.exceptionFlow = new FlowChain(exceptionExecutors.iterator());
    }

    public void execute() {
        execute( null );
    }

    public void execute( FlowData flowData ) {
        final Context context = new Context(flowData);
        try {
            preFlow.next( context );
            mainFlow.next( context );
        } catch (Exception e) {
            try {
                exceptionFlow.next(context);
            } catch (Exception ee) {
                //
            }
        }
    }

    public static FlowData newData() {
        return new Context(null);
    }

    private static class Context implements FlowData {

        private Map<String, Object> context = new HashMap<>();

        private FlowData parent;

        public Context( FlowData flowData ) {
            this.parent = flowData;
        }

        @Override
        public WrapData getData(String key) {
            WrapData data = null;
            if ( null == parent || Objects.isNull(data = parent.getData(key)) ) {
                if ( Objects.isNull(data = getByLevel(DataLevel.Runtime,key)) ) {
                    if ( Objects.isNull(data = getByLevel(DataLevel.Flow,key)) ) {
                        if ( Objects.isNull(data = getByLevel(DataLevel.System,key)) ) {
                            if ( Objects.isNull(data = getByLevel(DataLevel.Constant,key)) ) {
                                return WrapData.NULL;
                            }
                        }
                    }
                }
            }
            return data;
        }

        private WrapData getByLevel( DataLevel level , String key ) {
            if ( context.containsKey(key = level.modifyKey(key)) ) {
                return new WrapData(context.get(key));
            }
            return null;
        }

        @Override
        public void setData(DataLevel level, String key, Object data) {
            if ( null != parent ) {
                parent.setData( level , key , data );
                return;
            }
            context.put( level.modifyKey(key) , data );
        }

        public FlowInfo getExitFlow() {
            return getData("exitFlow" ).asValue(FlowInfo.class);
        }

        public void setExitFlow(FlowInfo exitFlow) {
            setData( DataLevel.Flow , "exitFlow" , exitFlow );
        }

        public Throwable getException() {
            return getData("exception").asValue(Throwable.class);
        }

        public void setException(Throwable latestException) {
            setData( DataLevel.Flow , "exception" , latestException );
        }
    }


    private static class FlowChain implements FlowInfo {

        private final Executor executor;

        private final FlowInfo next;

        public FlowChain(FlowInfo next , Executor executor ) {
            this.next = next;
            this.executor = executor;
        }

        public FlowChain(Iterator<Executor> iterator) {
            if ( iterator.hasNext() ) {
                this.executor = iterator.next();
                this.next = new FlowChain( iterator );
            }
            else {
                this.executor = null;
                this.next = null;
            }
        }

        @Override
        public void next (FlowData flowData) {
            if ( null == executor || null == next ) {
                return;
            }
            customNext((Context) flowData);
        }

        private void customNext( Context context ) {
            try {
                executor.execute( next , context );
            } catch (Exception e) {
                context.setException(e);
                context.setExitFlow(this);
                throw new RuntimeException(e);
            }
        }



    }

}
